/*
 * URL: http://code.google.com/p/nagatsuki-accounting-lib/
 */
/**
 * $Id: AccountMasterUtil.java 119 2009-07-28 10:00:36Z tanaka.akira.2006 $
 */
package com.googlecode.nagatsuki.accountinglib.util;

import static com.googlecode.nagatsuki.accountinglib.constants.AccountCategoryConstants.ASSET;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountCategoryConstants.EQUITY;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountCategoryConstants.EXPENSE;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountCategoryConstants.LIABILITY;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountCategoryConstants.PL;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountCategoryConstants.REVENUE;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.CAPITAL_STOCK;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.CURRENT_ASSET;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.CURRENT_LIAB;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.DEFERRED_ASSET;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.EXT_GAIN;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.EXT_LOSS;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.FIXED_ASSET;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.LONGTERM_LIAB;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.NON_OP_EXPS;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.NON_OP_INCOME;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.SALES;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.SALES_COST;
import static com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants.SELLING_AND_ADMIN_EXPS;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import com.googlecode.nagatsuki.accountinglib.constants.AccountSubCategoryConstants;
import com.googlecode.nagatsuki.accountinglib.exception.AccountMasterAlreadyExistsException;
import com.googlecode.nagatsuki.accountinglib.exception.UnknownAccountCategoryException;
import com.googlecode.nagatsuki.accountinglib.model.Account;

/**
 * 勘定マスタユーティリティ
 * <p>
 * 内部に勘定科目マスタを保持しています。<br>
 * クラスパスに存在する勘定科目マスタファイル {@link nagatsuki-account-lib-accountmaster.txt}
 * を読み込んでマスタとして内部にキャッシュします。<br>
 *
 * <br>
 *
 * 勘定科目マスタファイルは、 <code> "勘定科目名,勘定科目タイプ"</code> のようにCSVフォーマットとなります。 <br>
 * 上に来るほど貸借対照表などで上に表示されます。
 * </p>
 * <p>
 * <h2>勘定科目マスタファイル例:</h2>
 * <code>
 * 資産,流動資産,現金<br>
 * 資産,流動資産,預金<br>
 * 資産,流動資産,受取手形<br>
 * 資産,流動資産,売掛金<br>
 * 資産,流動資産,未収金<br>
 * 資産,流動資産,商品<br>
 * 資産,流動資産,製品<br>
 * 資産,流動資産,半製品<br>
 * 資産,流動資産,仕掛品<br>
 * 資産,流動資産,短期有価証券<br>
 * 資産,流動資産,前払金<br>
 * 資産,流動資産,仮払金<br>
 * 資産,流動資産,立替金<br>
 * 資産,流動資産,短期貸付金<br>
 * 資産,固定資産,土地<br>
 * 資産,固定資産,建物<br>
 * 資産,固定資産,構築物<br>
 * 資産,固定資産,付属設備<br>
 * 資産,固定資産,機械<br>
 * 資産,固定資産,装置<br>
 * 資産,固定資産,車両運搬具<br>
 * 資産,固定資産,出資金<br>
 * 資産,固定資産,投資有価証券<br>
 * 資産,固定資産,子会社株式<br>
 * 資産,固定資産,長期貸付金<br>
 * 資産,固定資産,保証金<br>
 * 資産,固定資産,敷金<br>
 * 資産,繰延資産,創立費<br>
 * 資産,繰延資産,開業準備費<br>
 * 資産,繰延資産,分担金<br>
 * 負債,流動負債,支払手形<br>
 * 負債,流動負債,買掛金<br>
 * 負債,流動負債,割引手形<br>
 * 負債,流動負債,未払金<br>
 * 負債,流動負債,未払税金<br>
 * 負債,流動負債,未払費用<br>
 * 負債,流動負債,短期借入金<br>
 * 負債,流動負債,預り金<br>
 * 負債,流動負債,仮受金<br>
 * 負債,流動負債,前受金<br>
 * 負債,固定負債,長期借入金<br>
 * 負債,固定負債,社債<br>
 * 負債,固定負債,長期預り保証金<br>
 * 資本,資本,資本金<br>
 * 資本,資本,未処分利益<br>
 * 資本,資本,元入金<br>
 * 収益,売上,売上<br>
 * 収益,営業外収益,不動産売上<br>
 * 収益,営業外収益,社保医業収入<br>
 * 収益,営業外収益,自由医業収入<br>
 * 収益,営業外収益,受取利息<br>
 * 収益,営業外収益,受取配当金<br>
 * 収益,営業外収益,有価証券売却益<br>
 * 収益,営業外収益,雑収入<br>
 * 収益,特別利益,固定資産売却益<br>
 * 収益,特別利益,投資有価証券売却益<br>
 * 収益,特別利益,過年度損益修正益<br>
 * 費用,売上原価,仕入<br>
 * 費用,販売費及び一般管理費,給与<br>
 * 費用,販売費及び一般管理費,雑給<br>
 * 費用,販売費及び一般管理費,賞与<br>
 * 費用,販売費及び一般管理費,役員報酬<br>
 * 費用,販売費及び一般管理費,役員賞与<br>
 * 費用,販売費及び一般管理費,退職金<br>
 * 費用,販売費及び一般管理費,役員退職金<br>
 * 費用,販売費及び一般管理費,法定福利費<br>
 * 費用,販売費及び一般管理費,福利厚生費<br>
 * 費用,販売費及び一般管理費,販売促進費<br>
 * 費用,販売費及び一般管理費,接待交際費<br>
 * 費用,販売費及び一般管理費,広告宣伝費<br>
 * 費用,販売費及び一般管理費,運送費<br>
 * 費用,販売費及び一般管理費,運賃<br>
 * 費用,販売費及び一般管理費,旅費交通費<br>
 * 費用,販売費及び一般管理費,水道光熱費<br>
 * 費用,販売費及び一般管理費,通信費<br>
 * 費用,販売費及び一般管理費,消耗品費<br>
 * 費用,販売費及び一般管理費,事務用品費<br>
 * 費用,販売費及び一般管理費,リース料<br>
 * 費用,販売費及び一般管理費,保険料<br>
 * 費用,販売費及び一般管理費,租税公課<br>
 * 費用,販売費及び一般管理費,減価償却費<br>
 * 費用,販売費及び一般管理費,修繕費<br>
 * 費用,販売費及び一般管理費,地代<br>
 * 費用,販売費及び一般管理費,家賃<br>
 * 費用,販売費及び一般管理費,雑費<br>
 * 費用,営業外費用,支払利息<br>
 * 費用,営業外費用,支払利息割引料<br>
 * 費用,営業外費用,社債利息<br>
 * 費用,営業外費用,割賦利息<br>
 * 費用,営業外費用,貸倒損<br>
 * 費用,営業外費用,雑損失<br>
 * 費用,特別損失,固定資産売却損<br>
 * 費用,特別損失,投資有価証券売却損<br>
 * 費用,特別損失,過年度損益修正損<br>
 * 利益,利益,利益<br>
 * 損失,損失,損失<br>
 * </code>
 * </p>
 *
 * @author tanaka.akira.2006
 *
 */
public final class AccountMasterUtil {

	private static final Logger logger = Logger
			.getLogger(AccountMasterUtil.class);

	/** 勘定マスタファイル */
	private static final String MASTER_FILE = "nagatsuki-account-lib-accountmaster.txt";
	/** 勘定マスタファイルの文字コード */
	private static final String CHARSET = "UTF-8";
	/** 勘定マスタファイルのデータセパレータ */
	private static final String SEPA = ",";

	/**
	 * メインカテゴリ
	 */
	private static final String TOKEN_ASSET = "資産";
	private static final String TOKEN_LIAB = "負債";
	private static final String TOKEN_EQUITY = "資本";
	private static final String TOKEN_REVENUE = "収益";
	private static final String TOKEN_EXPENSE = "費用";
	private static final String TOKEN_PL_ACCOUNT = "損益";

	/**
	 * サブカテゴリ
	 */
	public static final String TOKEN_SUB_CURRENT_ASSET = "流動資産";
	public static final String TOKEN_SUB_FIXED_ASSET = "固定資産";
	public static final String TOKEN_SUB_DEFERRED_ASSET = "繰延資産";
	public static final String TOKEN_SUB_CURRENT_LIAB = "流動負債";
	public static final String TOKEN_SUB_LONGTERM_LIAB = "固定負債";
	public static final String TOKEN_SUB_CAPITAL_STOCK = "資本";
	public static final String TOKEN_SUB_RETAINED_EARN = "利益";
	public static final String TOKEN_SUB_SALES = "売上";
	public static final String TOKEN_SUB_NON_OP_INCOME = "営業外収益";
	public static final String TOKEN_SUB_EXT_GAIN = "特別利益";
	public static final String TOKEN_SUB_SALES_COST = "売上原価";
	public static final String TOKEN_SUB_SELLING_AND_ADMIN_EXPS = "販売費及び一般管理費";
	public static final String TOKEN_SUB_NON_OP_EXPS = "営業外費用";
	public static final String TOKEN_SUB_EXT_LOSS = "特別損失";
	public static final String TOKEN_SUB_PL = "損益";

	/** 資産勘定科目マスタ */
	private static List<Account> assets = Collections
			.synchronizedList(new ArrayList<Account>(50));
	/** 負債勘定科目マスタ */
	private static List<Account> liabs = Collections
			.synchronizedList(new ArrayList<Account>(50));
	/** 純資産勘定科目マスタ */
	private static List<Account> equities = Collections
			.synchronizedList(new ArrayList<Account>(10));
	/** 収益勘定科目マスタ */
	private static List<Account> revenues = Collections
			.synchronizedList(new ArrayList<Account>(50));
	/** 費用勘定科目マスタ */
	private static List<Account> expenses = Collections
			.synchronizedList(new ArrayList<Account>(50));
	/** 損益勘定科目マスタ */
	private static Account pl;

	/**
	 * このクラスがロードされたとき
	 */
	static {
		reload();
	}

	/**
	 * 勘定マスタファイルを再読み込みします。
	 */
	public static void reload() {

		clear();

		logger.debug(String.format("勘定マスタファイル [%s] を読み込みます。", MASTER_FILE));
		InputStream is = ClassLoader.getSystemClassLoader()
				.getResourceAsStream(MASTER_FILE);
		InputStreamReader isr;
		int lineNo = 1;
		try {
			isr = new InputStreamReader(is, CHARSET);
			BufferedReader br = new BufferedReader(isr);
			String line = null;
			while ((line = br.readLine()) != null) {
				add(line, lineNo);
				lineNo++;
			}
		} catch (IOException e) {
			logger.error(String.format("勘定マスタファイル [%s] を読み込めませんでした。",
					MASTER_FILE), e);
		}
	}

	/**
	 * 保持している勘定マスタデータをクリアします。
	 */
	public static void clear() {
		assets.clear();
		liabs.clear();
		equities.clear();
		revenues.clear();
		expenses.clear();
	}

	private static void add(String line, final int lineNo) {
		if (StringUtils.isEmpty(line)) {
			logger.error(String.format("%s行目のレコードは空なのでスキップします。", lineNo));
			return;
		}

		String[] arr = line.split(SEPA);

		if (arr == null || arr.length != 3) {
			logger.error(String.format(
					"%s行目のレコードは書式が不正です。正しい書式は、'勘定カテゴリ,勘定科目名' です。", lineNo));
			return;
		}

		String accCategory = arr[0].trim();
		String accSubCategory = arr[1].trim();
		String accName = arr[2].trim();

		if (StringUtils.isEmpty(accCategory)) {
			logger
					.error(String.format("%s行目のレコードの勘定カテゴリが空なのでスキップします。",
							lineNo));
			return;
		}
		if (StringUtils.isEmpty(accSubCategory)) {
			logger.error(String.format("%s行目のレコードの勘定サブカテゴリが空なのでスキップします。",
					lineNo));
			return;
		}
		if (StringUtils.isEmpty(accName)) {
			logger.error(String.format("%s行目のレコードの勘定科目名が空なのでスキップします。", lineNo));
			return;
		}

		try {
			put(accCategory, accSubCategory, accName);
		} catch (UnknownAccountCategoryException e) {
			logger.error(String.format("%s行目の勘定カテゴリまたはサブカテゴリ [%s] は不正です。",
					lineNo, accCategory));
			return;
		} catch (AccountMasterAlreadyExistsException e) {
			logger.error(String.format("%s行目の勘定科目 [%s] は既に登録されています。", lineNo,
					accCategory));
			return;
		}

		return;
	}

	/**
	 * メモリに保持している勘定マスタに新しい勘定科目を追加します。
	 *
	 * @param category
	 *            e.g. "資産"
	 * @param subCategory
	 *            e.g. "流動資産"
	 * @param accountName
	 *            e.g. "現金"
	 * @throws AccountMasterAlreadyExistsException
	 * @throws UnknownAccountCategoryException
	 */
	public static void put(String category, String subCategory,
			String accountName) throws AccountMasterAlreadyExistsException,
			UnknownAccountCategoryException {

		// 既に登録されていないかチェック
		Account acc = get(accountName);
		if (acc != null) {
			throw new AccountMasterAlreadyExistsException();
		}

		if (category.equals(TOKEN_ASSET)) {
			if (subCategory.equals(TOKEN_SUB_CURRENT_ASSET)) {
				// 流動資産
				assets.add(new Account(ASSET, CURRENT_ASSET, accountName));
			} else if (subCategory.equals(TOKEN_SUB_FIXED_ASSET)) {
				// 固定資産
				assets.add(new Account(ASSET, FIXED_ASSET, accountName));
			} else if (subCategory.equals(TOKEN_SUB_DEFERRED_ASSET)) {
				// 繰延資産
				assets.add(new Account(ASSET, DEFERRED_ASSET, accountName));
			}
		} else if (category.equals(TOKEN_LIAB)) {
			if (subCategory.equals(TOKEN_SUB_CURRENT_LIAB)) {
				// 流動負債
				liabs.add(new Account(LIABILITY, CURRENT_LIAB, accountName));
			} else if (subCategory.equals(TOKEN_SUB_LONGTERM_LIAB)) {
				// 固定負債
				liabs.add(new Account(LIABILITY, LONGTERM_LIAB, accountName));
			}
		} else if (category.equals(TOKEN_EQUITY)) {
			if (subCategory.equals(TOKEN_SUB_CAPITAL_STOCK)) {
				// 資本
				equities.add(new Account(EQUITY, CAPITAL_STOCK, accountName));
			}
		} else if (category.equals(TOKEN_REVENUE)) {
			if (subCategory.equals(TOKEN_SUB_SALES)) {
				// 売上
				revenues.add(new Account(REVENUE, SALES, accountName));
			} else if (subCategory.equals(TOKEN_SUB_NON_OP_INCOME)) {
				// 営業外収益
				revenues.add(new Account(REVENUE, NON_OP_INCOME, accountName));
			} else if (subCategory.equals(TOKEN_SUB_EXT_GAIN)) {
				// 特別利益
				revenues.add(new Account(REVENUE, EXT_GAIN, accountName));
			}
		} else if (category.equals(TOKEN_EXPENSE)) {
			if (subCategory.equals(TOKEN_SUB_SALES_COST)) {
				// 売上原価
				expenses.add(new Account(EXPENSE, SALES_COST, accountName));
			} else if (subCategory.equals(TOKEN_SUB_SELLING_AND_ADMIN_EXPS)) {
				// 販売費及び一般管理費
				expenses.add(new Account(EXPENSE, SELLING_AND_ADMIN_EXPS,
						accountName));
			} else if (subCategory.equals(TOKEN_SUB_NON_OP_EXPS)) {
				// 営業外費用
				expenses.add(new Account(EXPENSE, NON_OP_EXPS, accountName));
			} else if (subCategory.equals(TOKEN_SUB_EXT_LOSS)) {
				// 特別損失
				expenses.add(new Account(EXPENSE, EXT_LOSS, accountName));
			}
		} else if (category.equals(TOKEN_PL_ACCOUNT)) {
			if (subCategory.equals(TOKEN_SUB_PL)) {
				// 損益
				pl = new Account(PL, AccountSubCategoryConstants.PL,
						accountName);
			}
		} else {
			throw new UnknownAccountCategoryException();
		}
	}

	private static final Account get(final String accountName) {
		for (Account acc : assets) {
			if (acc.name.equals(accountName)) {
				return acc;
			}
		}
		for (Account acc : liabs) {
			if (acc.name.equals(accountName)) {
				return acc;
			}
		}
		for (Account acc : equities) {
			if (acc.name.equals(accountName)) {
				return acc;
			}
		}
		for (Account acc : revenues) {
			if (acc.name.equals(accountName)) {
				return acc;
			}
		}
		for (Account acc : expenses) {
			if (acc.name.equals(accountName)) {
				return acc;
			}
		}
		if (pl != null && pl.name.equals(accountName)) {
			return pl;
		}
		return null;
	}

	public static final List<Account> getAssets() {
		return assets;
	}

	public static final List<Account> getLiabilities() {
		return liabs;
	}

	public static final List<Account> getEquities() {
		return equities;
	}

	public static final List<Account> getRevenues() {
		return revenues;
	}

	public static final List<Account> getExpenses() {
		return expenses;
	}

	public static final Account getPl() {
		return pl;
	}

}
